/**
 * 维护企业微信列表
 *
 * 【企业微信运行状态】
 * 状态转换图：
 * [接口返回企业微信 | (MQTT 收到登录消息 => Preparing)
 *        => Normal => Destroying => Removed
 *                  => LoggingOut => Removed
 *
 * 部分异常状态会持久化记录起来，以保证刷新页面，重新识别到企业微信后，维持之前记录的状态。
 *
 * - 应用启动时，先通过接口拉取已登录企业微信列表。
 *   接口目前不会返回企业微信状态，先检查本地持久化的异常状态记录，以记录下来的状态来初始化企业微信；若没有记录，视为 Normal 状态。
 *
 * - 通过接收 MQTT 推送的“登录成功”消息，发现新登录的企业微信。
 *   新登录的企业微信会进入 Preparing 状态（因为企业微信本身需要一些时间初始化，才能正常运作），之后转为 Normal。
 *  （如果发现此企业微信已经在列表里了，则先将其从列表里彻底移除，再以新登录的状态添加回来）
 *
 * - 正常的企业微信因手动退出（销毁容器）、手机端切号或其他异常原因退出的，会进入 Destroying / LoggingOut 状态，
 *   因为退出后 MQTT 可能仍会推送残留的消息，这两个状态下会忽略这些消息。
 *   过了屏蔽期后，容器彻底移除（Removed）。
 *
 * 【确认企业微信退出的几种情况】
 * - 通过界面操作手动移除，实际实现方式是销毁容器。
 *   因为容器销毁有延迟，需要有一个“消息屏蔽期”，以避免收到相关消息认为企业微信再次登录.
 *
 * - 因用户操作而退出。
 *   此情况会收到一条 11027 消息。并不能保证此消息后一定不会收到残留的此 wxid 的消息，因此也要进入“屏蔽期“。
 *
 */
import { getLogger, Service, formatSuccess } from '@gnlab/utils'
import { MemSubject, mmap } from '@gnlab/utils/rx'
import dayjs from 'dayjs'
import { OrderedMap, OrderedSet } from 'immutable'
import { Subject } from 'rxjs'
import { filter, map, buffer, debounceTime } from 'rxjs/operators'
import { makeHTTPSAvatar } from '@/lib/https'
import type { Services } from '@/services'
import { API } from '@/services/common/api'
import type { RecvMessage } from '@/services/common/qywx-mqtt/protocol'
import { Status as ContainerStatus } from '@/services/container'
import { BatchLogin } from './batch-login'
import { QywxFocus } from './focus'
import { QywxStatus } from './status'
import { QywxTransfers } from './transfers'
import type {
  Event,
  QywxNotice,
  QywxInfo,
  QywxStatusInfo,
  RawQywxInfo,
  RespAccountQywx,
  RescanInfo,
} from './types'
import {
  Status,
  runningStatus,
  LOAD_INTERVAL,
  DESTROY_BLOCKING,
  LOGOUT_BLOCKING,
  formatQywxInfo,
  formatAccountQywx,
} from './types'

export * from './types'

const logger = getLogger('services/qywxs')

/*
TODO: 针对企业微信与客户转交，还有一些特殊情况要处理：
- 例如意外情况下，先把客户转给一个人，又把企业微信也转给那个人
- 先把客户转给一个人，又把企业微信转给另一个人（转交数据是否要修改）
- 先把客户转给一个人，又在另一个人那里登录了企业微信

对于前两种情况，也许应该在企业微信转交时，由后端先解除所有相关联系人的转交（这个可能已经有了）
最后一种情况，这边发现此类企业微信登录时，立刻触发联系人退回
*/

/**
 * 请求获取企业微信列表
 * - 不指定 userId 则获取所有账号的企业微信列表
 * - 此函数不进行格式化，因为部分场景需要获取原始数据，若需格式化后的数据可使用 getFormattedQywxs()
 */
export async function getQywxs(userId = '', onlineOnly = true) {
  return API<RespAccountQywx[]>('/WxUser/WxUserList', {
    data: {
      userId,
      isOnline: onlineOnly,
    },
  })
}

export async function getFormattedQywxs(userId = '', onlineOnly = true) {
  return formatSuccess(await getQywxs(userId, onlineOnly), qywxs => qywxs.map(formatAccountQywx))
}

export class Qywxs extends Service<Services> {
  batchLogin = new BatchLogin()
  focus = new QywxFocus()
  transfers = new QywxTransfers()

  get auth() {
    return this.services.auth
  }
  get accounts() {
    return this.auth.observing.accounts$.value
  }
  get communicate() {
    return this.services.mqtt.communicate
  }
  get featAccounts() {
    return this.services.features.accounts
  }
  get corpWhitelist() {
    return this.services.features.corpWhitelist
  }
  get container() {
    return this.services.container
  }
  get contacts() {
    return this.services.contacts
  }
  get inServiceMode() {
    return this.auth.inServiceMode$.value!
  }

  registered() {
    void this.auth.bizAccountIds.then(() => {
      if (this.inServiceMode) return // 客服模式下不加载企微
      this.initLoad()
      this.initLoginHandler()
      this.initExitHandler()
      this.initNoticeLogging()
      this.initListenRescan()
    })
  }

  // 企业微信列表
  // removed 的企业微信直接移除，不保留在这里
  qywxs = new MemSubject(OrderedMap<string, QywxStatusInfo>())

  // 当前处于运行状态的企业微信列表
  // - 此列表中的企业微信未必可以用来执行操作（例如 Preparing 状态）
  // - 不在此列表中的企业微信均视为不存在
  alives = this.qywxs.pipe(
    mmap(qywxs =>
      qywxs.filter(qywx => qywx.status === Status.Normal || qywx.status === Status.Preparing),
    ),
  )

  // 当前可正常使用的企业微信列表
  usables = this.qywxs.pipe(mmap(qywxs => qywxs.filter(qywx => qywx.status === Status.Normal)))

  // 当前可正常使用的企业微信 ID 列表
  usableWxids = this.usables.pipe(mmap(infos => [...infos.keys()]))

  // 当前所有企业微信涉及到的企业列表
  corps = this.usables.pipe(
    mmap(qywxs => OrderedMap([...qywxs.values()].map(qywx => [qywx.corpid, qywx.corpname]))),
  )

  // 当前所有企业微信涉及到的 corpid 集合
  corpIds = this.corps.pipe(mmap(corps => OrderedSet(corps.keys())))

  // 推送企业微信状态变化事件
  events = new Subject<Event>()

  // 推送新企业微信的信息
  prepared = this.events.pipe(
    filter(event => event.type === 'prepared'),
    map(event => event.info),
  )
  // 每隔一段时间统一推送一批新企业微信的信息
  bufferPrepared = this.prepared.pipe(buffer(this.prepared.pipe(debounceTime(500))))
  // 推送无效了的企业微信的信息
  // （与 prepared 不同，此信息不进行 buffer，因为通常新企业微信要批量统一处理，但移除企业微信时却要不经等待立刻处理）
  notAlived = this.events.pipe(
    filter(event => ['destroying', 'loggingOut', 'removed'].includes(event.type)),
    map(event => event.info),
  )

  // 推送二次扫码通知
  // 详见 initListenRescan()
  private readonly rescan$$ = new Subject<RescanInfo>()
  readonly rescan$ = this.rescan$$.asObservable()

  /**
   * 推送企业微信异常消息通知
   */
  get notices() {
    return this.communicate.hook<{ title: string; content: string }>(11095).pipe(
      map(item => {
        const {
          data: { title, content },
          message: { wxid, hostname },
        } = item
        return { wxid, hostname, title, content } as QywxNotice
      }),
    )
  }

  // 维护企业微信状态的实例列表
  // removed 状态的实例直接移除，不保留在这里
  protected status = new Map<string, QywxStatus>()

  /**
   * ===========================================
   * 发现企业微信
   * ===========================================
   */

  /**
   * 定时拉取当前账号最新的企业微信列表
   */
  protected initLoad() {
    void this.loadQywxs()
    setInterval(async () => this.loadQywxs(), LOAD_INTERVAL)
  }

  /**
   * 拉取最新的企业微信列表
   */
  async loadQywxs() {
    if (this.inServiceMode) return

    // 因为 getQywxs() 不支持传多个 account，accountIds.length > 1 的情况下，
    // 会尝试拉取所有账号的企微，然后再根据 accountIds 筛选
    const accountIds = await this.auth.bizAccountIds
    const res = await getFormattedQywxs(accountIds.length === 1 ? accountIds[0] : '')
    if (res.success) {
      const loadedQywxs =
        accountIds.length > 1
          ? res.data.filter(qywx => accountIds.includes(qywx.accountId))
          : res.data

      // 在线状态的企业微信，若未出现在最新返回的列表里，标记为已被删除
      this.qywxs.value
        .filter(
          (qywx, wxid) =>
            (qywx.status === Status.Preparing || qywx.status === Status.Normal) &&
            !loadedQywxs.find(v => v.wxid === wxid),
        )
        .forEach((qywx, wxid) => {
          const status = this.status.get(wxid)
          if (status) status.goStatus(Status.Removed)
        })

      // 接口返回的企业微信都视为已经“准备完成”了
      loadedQywxs.forEach(qywx => this.detected(qywx))
    }
  }

  /**
   * 监听新登录的企业微信
   */
  protected initLoginHandler() {
    // 先推送的 11026 消息缺少 corp_name 和 corp_short_name；后续的 11179 提供了它们
    this.communicate.hook<RawQywxInfo>(11026).subscribe(this.onLoggedIn.bind(this))
    this.communicate
      .hook<RawQywxInfo>(11179)
      .subscribe(({ data, message: { accountId } }) => this.supplementInfo(accountId, data))
  }

  protected async onLoggedIn({
    data,
    message: { hostname, accountId },
  }: {
    data: RawQywxInfo
    message: RecvMessage<unknown>
  }) {
    const wxid = data.user_id
    const info = formatQywxInfo(accountId, hostname, data)

    // 若企业微信已有记录，先将其移除
    const existsStatus = this.status.get(wxid)
    if (existsStatus) existsStatus.goStatus(Status.Removed)

    // 检查登录白名单
    const passWhitelist = this.corpWhitelist.check(info.corpid)
    if (passWhitelist === false) {
      this.container.destroy([info.hostname]) // 退出此企业微信。因为它还没被加到 qywxs 列表里，直接调用底层的方法删除容器
      logger.info('企业微信主体不在白名单内，禁止登录', info)
      this.events.next({ type: 'blocked', info })
      return
    }
    if (passWhitelist === null) logger.info('白名单未加载成功，无法判断企业主体是否符合', info) // 此时依然放行登录

    // 确认新企业微信是否需要进入 preparing 状态
    const needPrepareRes = await API<{ IsNeedLoading: boolean }>('/WxUser/IsNeedLoading', {
      data: { WxId: wxid },
    })
    const needPrepare = needPrepareRes.success ? needPrepareRes.data.IsNeedLoading : true

    // 正式添加新企业微信
    this.detected(info, needPrepare)
  }

  /**
   * 处理后端返回的或新登录的企业微信
   * - wxid 不合法（例如为空）会被忽略
   * - 已经有登记的会被忽略，无论其登记为哪种状态
   */
  private detected(info: QywxInfo, preparing = false) {
    const { wxid, hostname } = info
    if (this.qywxs.value.has(wxid)) return
    logger.info('发现企业微信', `${wxid}@${hostname}`)

    // 若当前“可用容器”就是此企业微信登录的容器，将此容器标记为"已使用"
    const usableContainer = this.container.usable.value
    if (
      usableContainer.status === ContainerStatus.Normal &&
      usableContainer.data.hostname === hostname
    )
      this.container.forget()

    const status = new QywxStatus(wxid, hostname, preparing)
    this.status.set(wxid, status)

    const saveInfo = {
      ...info,
      status: status.current.value as Exclude<Status, Status.Removed>,
    }
    this.qywxs.next(this.qywxs.value.set(wxid, saveInfo))
    this.events.next({ type: 'found', info: saveInfo })

    // 监听状态变化（初始也会触发一次）
    status.current.subscribe(current => this.handleStatus(wxid, current))
  }

  /* 处理状态变化 */
  protected handleStatus(wxid: string, status: Status) {
    const info = this.qywxs.value.get(wxid)!

    const logId = `${info.wxid}@${info.hostname}`
    if (status === Status.Preparing) logger.info('企业微信准备中', logId)
    if (status === Status.Normal) logger.info('企业微信已可以使用', logId)
    if (status === Status.Destroying) logger.info('企业微信已注销，忽略短时间内的消息', logId)
    if (status === Status.LoggingOut) logger.info('企业微信已退出，忽略短时间内的消息', logId)
    if (status === Status.Removed) logger.info('企业微信已彻底移除', logId)

    // 推送通知
    const eventTypes = {
      [Status.Preparing]: 'preparing',
      [Status.Normal]: 'prepared',
      [Status.Destroying]: 'destroying',
      [Status.LoggingOut]: 'loggingOut',
      [Status.Removed]: 'removed',
    }
    const eventType = eventTypes[status] as
      | 'preparing'
      | 'prepared'
      | 'destroying'
      | 'loggingOut'
      | 'removed'
    this.events.next({ type: eventType, info })

    if (status === Status.Removed) {
      this.qywxs.next(this.qywxs.value.delete(wxid))
      this.status.delete(wxid)
    } else if (status !== info.status) {
      this.qywxs.next(this.qywxs.value.set(wxid, { ...info, status }))
    }
  }

  /**
   * 已登录的企微补充信息
   * （因为初始推送的信息缺少企业名）
   */
  supplementInfo(accountId: string, raw: RawQywxInfo) {
    const wxid = raw.user_id
    const existsQywx = this.qywxs.value.get(wxid)
    if (!existsQywx) return void logger.error('找不到企业微信，无法补充其信息', wxid)

    const info = formatQywxInfo(accountId, existsQywx.hostname, raw)
    logger.info('补充企业微信信息', `${wxid}@${existsQywx.hostname}`)
    const updated = this.qywxs.value.set(wxid, { ...existsQywx, ...info })
    this.qywxs.next(updated)
  }

  /**
   * ===========================================
   * 处理企业微信异常情况
   * ===========================================
   */

  /**
   * 处理因切换账号等原因退出
   */
  protected initExitHandler() {
    this.communicate.hook(11027).subscribe(({ message: { wxid } }) => {
      const status = this.status.get(wxid ?? '')
      if (status) status.goStatus(Status.LoggingOut, Date.now() + LOGOUT_BLOCKING)
    })
  }

  /**
   * 将企业微信异常消息输出到日志
   */
  protected initNoticeLogging() {
    this.notices.subscribe(notice => logger.warn('企业微信提醒', notice))
  }

  /**
   * 风控有时会要求企微登录后，二次扫指定的二维码。
   * - 部分企微登录后 25 分钟会收到此推送
   * - 出现此推送时，会先出现 11027 企微退出消息，若扫码成功，企微会恢复回来
   *   因此在处理此推送时，对应的企微大概率并不会在企微列表里。
   * - 必须在 2 分钟内扫码，否则二维码失效，需要从头重新登录（且很可能会再次收到此推送）
   */
  private initListenRescan() {
    this.communicate
      .manage<{ Qrcode: string; UserName: string; Avatar: string }>(60008)
      .subscribe(message => {
        logger.info('企微要求二次扫码', message)
        const { wxid } = message
        if (wxid === null) return
        this.rescan$$.next({
          wxid,
          name: message.data.UserName,
          avatar: makeHTTPSAvatar(message.data.Avatar || ''),
          qrcode: message.data.Qrcode,
          limit: dayjs().add(10, 'minutes'),
        })
      })
  }

  /**
   * ===========================================
   * 企业微信相关操作
   * ===========================================
   */

  /**
   * 手动退出指定企业微信（原理上是销毁容器）
   * 此方法会自行检查传进来的 wxid 是否合法，无需在调用前提前确认
   *
   * 若 soft=true，则不会真正执行“销毁容器”的操作，只是将其标记成“已销毁”
   */
  logout(wxids: string | string[], soft = false) {
    if (!Array.isArray(wxids)) wxids = [wxids]
    const items = wxids
      .map(wxid => this.qywxs.value.get(wxid))
      .filter(
        (info): info is QywxStatusInfo => info !== undefined && runningStatus.has(info.status),
      )

    for (const info of items) {
      this.status.get(info.wxid)!.goStatus(Status.Destroying, Date.now() + DESTROY_BLOCKING)
    }

    if (!soft) this.container.destroy(items.map(info => info.hostname))
  }

  /**
   * 退出所有企业微信
   */
  logoutAll() {
    this.logout([...this.qywxs.value.keys()])
  }

  /**
   * 取得（当前主账号下）所有账号已登录的企业微信的列表
   */
  async getAllAccountQywxs(onlineOnly = true, refreshAccounts = false) {
    const [accounts, qywxsRes] = await Promise.all([
      this.featAccounts.getAccounts(refreshAccounts),
      getFormattedQywxs('', onlineOnly),
    ])
    if (qywxsRes.success) {
      const qywxs = qywxsRes.data
        // 忽略账户列表里没出现的账号的企业微信（可能是其他登录方式或已移除的账号登的）
        .filter(qywx => accounts.find(acc => acc.id === qywx.accountId))
      return { accounts, qywxs }
    } else {
      logger.warn('加载各账号企业微信列表失败', qywxsRes.error)
      return { accounts, qywxs: [] }
    }
  }

  /**
   * 返回（当前主账号下）所有企业微信的 wxid
   */
  async getAllAccountWxids(onlineOnly = true, refreshAccounts?: boolean) {
    const { qywxs } = await this.getAllAccountQywxs(onlineOnly, refreshAccounts)
    return new Set(qywxs.map(v => v.wxid))
  }

  /**
   * 管理员退出自己登录的、或其他账号登录的任意一个企业微信
   */
  adminLogout(items: Pick<QywxInfo, 'wxid' | 'hostname'>[]) {
    // 自己账号登录的，调用正常的退出函数
    this.logout(items.map(info => info.wxid))

    // 其他账号登录的，直接销毁容器
    const outerItems = items.filter(info => !this.qywxs.value.has(info.wxid))
    logger.info('管理员登出其他账号的企业微信：', outerItems)
    this.container.destroy(outerItems.map(info => info.hostname))
  }

  /**
   * 对于登录过又掉线了的企微，用之前的容器重新登录
   */
  async containerLogin(hostname: string,isRefresh:boolean) {
    return API<{ Status: number; HostName: string; QrCode: string }>(
      '/Container/StartOrCreateContainer',
      {
          data: { hostname, isRefresh },
      },
    )
  }

  /**
   * 管理员对指定企业微信执行”容器操作“
   * restart: 重启
   * offline: 下线
   * online: 上线
   */
  async adminContainerAction(action: 'reboot' | 'offline' | 'online', hostnames: string[]) {
    const api = {
      reboot: '/Container/RestartContainer',
      offline: '/Container/StopContainer',
      online: '/Container/StartContainer',
    }[action]
    return API(api, { data: { hostname: hostnames } })
  }
}
